KubeJS Compat
Epiphany provides full KubeJS compatibility, allowing modpack authors to listen to events, call APIs, and query data through JavaScript scripts. All functionality is registered via the standard KubeJS plugin mechanism.
TIP
All scripts reside in the server_scripts/ directory and run on the server side only.
INFO
All concepts introduced in this section correspond one-to-one with Custom Events and Manager API. Unless otherwise noted, behavior is identical to the Java side.
Overview
Epiphany provides two entry points via the KubeJS plugin:
| Namespace | Purpose | Source |
|---|---|---|
EpiphanyEvents.* | Listen to events (15 events related to Module/Insight/Epiphany/Aptitude) | KubeJS EventGroup |
Epiphany.* | Call API (Manager methods + data queries) | KubeJS Binding |
Namespace separation
Event listening uses EpiphanyEvents.*, API calls use Epiphany.*. The two are deliberately placed in different namespaces to avoid name collisions.
Plugin entry point: ink.myumoon.epiphany.event.kubejs.EpiphanyKubeJSPlugin, discovered via kubejs.plugins.txt.
Events (EpiphanyEvents.*)
All events carry a player field (ServerPlayer). Each event carries relevant id fields (e.g., moduleId, insightId, epiphanyId) for scripts to read.
Naming Convention
| Suffix | Meaning | Cancellable |
|---|---|---|
Present tense (e.g., moduleUnlock, moduleSelect, moduleComplete) | Pre event, cancel with event.cancel() | ✅ |
Past tense (e.g., moduleUnlocked, moduleSelected, moduleCompleted) | Post event, notification only | ❌ |
Cancellation mechanism
Pre events are cancelled using KubeJS-standard event.cancel(), not the Java-side setCanceled(true). The plugin internally bridges to native NeoForge events.
Module Events
| Event Name | Type | When It Fires |
|---|---|---|
moduleUnlock | Pre (cancellable) | Module is about to be unlocked |
moduleUnlocked | Post | Module has been unlocked |
moduleSelect | Pre (cancellable) | Module is about to be selected |
moduleSelected | Post | Module has been selected |
moduleComplete | Pre (cancellable) | Module is about to be completed |
moduleCompleted | Post | Module has been completed |
Insight Events
Cascading
After insightSelected fires, if all insights in the owning module are now unlocked, it automatically chains into Module's moduleComplete / moduleCompleted. A single insightSelect call can trigger a full 4-event chain.
| Event Name | Type | Description |
|---|---|---|
insightSelect | Pre (cancellable) | Insight is about to be unlocked |
insightSelected | Post | Insight has been unlocked |
Epiphany Events
| Event Name | Type | Description |
|---|---|---|
epiphanyUnlock | Pre (cancellable) | Epiphany is about to be unlocked |
epiphanyUnlocked | Post | Epiphany has been unlocked |
epiphanySelect | Pre (cancellable) | Epiphany is about to be activated |
epiphanySelected | Post | Epiphany has been activated |
Aptitude Events (All Post)
Trigger frequency differences
aptitudeLevelUp fires once per +1 Insight Point (one addAptitude call can trigger it multiple times). In contrast, insightPointsChanged is a settlement-level event — regardless of how many internal level-ups occur in one call, it fires only once. Use the latter for notifications, the former for per-point tracking.
| Event Name | Description |
|---|---|
aptitudeChanged | When aptitude value changes |
aptitudeLevelUp | When the bar fills and an Insight Point is earned (once per +1) |
insightPointsChanged | When the Insight Point balance changes (settlement-level, once per call) |
Bindings (Epiphany.*)
All methods execute synchronously on the server side. The player parameter must be of type ServerPlayer.
Utilities
Epiphany.id(namespace, path) // Construct a ResourceLocation
Epiphany.id('mymod', 'foo') // → 'mymod:foo'You can also use KubeJS string literals like 'mymod:foo' — both are equivalent.
Module
| Method | Description |
|---|---|
isModuleUnlocked(player, id) | Query: is the module unlocked? |
isModuleSelected(player, id) | Query: is the module selected? |
isModuleCompleted(player, id) | Query: is the module completed? |
moduleUnlock(player, id) | Unlock the module |
moduleLock(player, id) | Lock the module |
moduleSelect(player, id) | Select the module (consumes Insight Points) |
moduleForceSelect(player, id) | Force select (ignores cost/conditions/cap) |
moduleComplete(player, id) | Complete the module (requires all insights unlocked) |
moduleForceComplete(player, id) | Force complete |
moduleReset(player, id) | Reset the module (refunds Insight Points, removes rewards) |
Insight
| Method | Description |
|---|---|
isInsightSelected(player, id) | Query: is the insight unlocked? |
isInsightModuleSelected(player, id) | Query: is the insight's owning module selected? |
insightSelect(player, insightId, moduleId) | Unlock the insight (consumes Insight Points) |
insightForceSelect(player, insightId, moduleId) | Force unlock (ignores cost/prerequisites) |
insightReset(player, id) | Reset the insight |
Epiphany
| Method | Description |
|---|---|
isEpiphanyUnlocked(player, id) | Query: is the epiphany unlocked? |
isEpiphanySelected(player, id) | Query: is the epiphany activated? |
epiphanyUnlock(player, id) | Unlock the epiphany |
epiphanyLock(player, id) | Lock the epiphany |
epiphanySelect(player, id) | Activate the epiphany (consumes 1 slot) |
epiphanyForceSelect(player, id) | Force activate (ignores slot limits) |
epiphanyReset(player, id) | Reset the epiphany |
Tools
| Method | Returns | Description |
|---|---|---|
getEpiphanySlots(player) | int | Epiphany Slots unlocked by the player |
getUsedEpiphanySlots(player) | int | Epiphany Slots currently occupied |
getMaxEpiphanySlots() | int | Configured hard cap on selectable Epiphanies |
getSelectedModuleCount(player) | int | Modules the player currently has selected |
getMaxSelectedModules() | int | Configured hard cap on simultaneously selected Modules |
Reset
| Method | Description |
|---|---|
resetAll(player) | Remove all rewards and Epiphany data, then restore initially SELECTABLE entries |
resetSelections(player) | Clear selections and rewards, preserve aptitude, refund selection costs, then restore initially SELECTABLE entries |
Event behavior
These methods are equivalent to /epiphany reset all and /epiphany reset select. They intentionally fire no aptitude, Insight Point, or selection events.
Aptitude / Points
| Method | Returns | Description |
|---|---|---|
getAptitude(player) | long | Current aptitude |
getInsightPoints(player) | int | Available Insight Points |
getTotalInsightPointsSpent(player) | int | Total Insight Points spent |
setAptitude(player, value) | — | Set aptitude |
addAptitude(player, amount) | — | Add an exact aptitude amount without applying the gain multiplier (may trigger level-ups) |
addAptitudeWithMultiplier(player, amount) | — | Add aptitude after applying the player's aptitude_gain_multiplier; the scaled amount is rounded down |
setInsightPoints(player, value) | — | Set Insight Points |
addInsightPoints(player, amount) | — | Add or subtract Insight Points (result clamped to >= 0) |
fillAptitude(player) | long | Add enough aptitude to reach the next Insight Point threshold; returns the amount added |
getRequiredForNextPoint(player) | long | Get the aptitude threshold for the player's next Insight Point |
calcRequiredAptitude(totalSpent, points) | long | Calculate aptitude needed for next Insight Point |
Aptitude Source (Custom Aptitude Sources)
| Method | Returns | Description |
|---|---|---|
grantAptitude(player, behaviorId, targetId, registry) | boolean | Grant aptitude per datapack rules |
resolveAptitudeSource(player, behaviorId, targetId, registry) | Resolution | Pure query without granting |
Pass null for the registry parameter (used for resolving tag references) if not needed.
Data Queries (Datapack Registry)
Look up one datapack-defined entry by ID. Returns null when not found:
| Method | Return Type |
|---|---|
getModule(id) | ModuleData |
getInsight(id) | InsightData |
getEpiphany(id) | EpiphanyData |
getPath(id) | PathData |
You can also list every registered Module, Epiphany, or Insight:
| Method | Return Type | Description |
|---|---|---|
allModuleIds() | ResourceLocation[] | All Module IDs, sorted |
allModules() | ModuleData[] | All Module definitions |
allEpiphanyIds() | ResourceLocation[] | All Epiphany IDs, sorted |
allEpiphanies() | EpiphanyData[] | All Epiphany definitions |
allInsightIds() | ResourceLocation[] | All Insight IDs, sorted |
allInsights() | InsightData[] | All Insight definitions |
Iterate as arrays
The bulk query methods return Java arrays rather than Lists. Use indexed access in KubeJS; Rhino's List.get(i) can return incorrect elements.
let ids = Epiphany.allModuleIds();
for (let i = 0; i < ids.length; i++) {
console.info(ids[i]);
}
let insights = Epiphany.allInsights();
for (let i = 0; i < insights.length; i++) {
console.info(insights[i].cost());
}The returned records' fields can be accessed directly. For example:
let module = Epiphany.getModule('mymod:combat');
if (module) {
console.info(module.name); // Optional<Component>
console.info(module.description); // Optional<Component>
console.info(module.initialState); // 'locked' or 'selectable'
console.info(module.weight); // number
module.insights.forEach(entry => {
console.info(entry.id, entry.depth); // insight ID + depth
});
}Optional fields and lang fallback
The name, description, and *_description fields exposed on records are raw Optionals — they contain whatever was written in the datapack JSON, without applying the default lang fallback logic. For example:
module.name→Optional<Component>, may be empty (if JSON omittedname)module.name.get().getString()→ returns the JSON literal (or translation key string), but does not go through lang translation fallback
If you provide multilingual support via resource packs, use each data type's effectiveXxx(id) method:
let id = 'mymod:combat';
let module = Epiphany.getModule(id);
if (module) {
// Raw — may be empty
let rawName = module.name; // Optional<Component>
// Resolved — goes through full lang fallback chain
let displayName = module.effectiveName(id).getString();
}